' ***********************************************************************
' Assembly         : Ioc
' Author           : Tim Ford
' Created          : 10-02-2013
'
' Last Modified By : Tim Ford
' Last Modified On : 10-02-2013
' ***********************************************************************
' <copyright file="AutoImplementor.vb" company="Red Baron Solutions">
'     Red Baron Solutions. All rights reserved.
' </copyright>
' <summary></summary>
' ***********************************************************************



Imports System.Reflection
Imports System.Reflection.Emit
Imports System.Threading
Imports System.Globalization



Namespace Ioc
    ''' <summary>
    ''' Returns objects that implement an interface, without the need to manually create a type 
    ''' that implements the interface
    ''' </summary>
    Public NotInheritable Class AutoImplementor
        Private Sub New()
        End Sub
        ''' <summary>
        ''' All of the types generated for each interface.  This dictionary is indexed by the 
        ''' interface's type object
        ''' </summary>
        Private Shared ReadOnly InterfaceImplementations As New Dictionary(Of Type, Type)()

        ''' <summary>
        ''' Returns an object that implement an interface, without the need to manually create a
        ''' type that implements the interface
        ''' </summary>
        ''' <typeparam name="T">T must be an interface that is public.</typeparam>
        ''' <returns>An object that implements the T interface</returns>
        Public Shared Function GetNewInstance(Of T)() As T
            Dim type As Type = GetType(T)

            ' If the type that implements the isn't created, create it
            If Not InterfaceImplementations.ContainsKey(type) Then
                CreateTypeFor(type)
            End If

            ' Now that the type exists to implement the interface, use the Activator to create an 
            ' object
            Return DirectCast(Activator.CreateInstance(InterfaceImplementations(type)), T)
        End Function

        ''' <summary>
        ''' Initializes the <see cref="AutoImplementor" /> class.	
        ''' </summary>
        ''' <remarks></remarks>
        Shared Sub New()
            ' Initialize an assembly and module builder for use for all generated classes
            Dim appDomain As AppDomain = Thread.GetDomain()
            Dim assemblyName As New AssemblyName() With {.Name = "InterfaceObjectFactoryAsm"}

            Dim assemblyBuilder As AssemblyBuilder = appDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave)

            ' This ModuleBuilder is used for all generated classes.  It's only constructed once, 
            'the first time that the InterfaceObjectFactory is used
            ModuleBuilder = assemblyBuilder.DefineDynamicModule("InterfaceObjectFactoryModule", "InterfaceObjectFactory.dll", True)
        End Sub

        ''' <summary>
        ''' The module builder used for all types constructed
        ''' </summary>
        Private Shared ReadOnly ModuleBuilder As ModuleBuilder

        ''' <summary>
        ''' Creates a method that will generate an object that implements the interface for the 
        ''' given type.
        ''' </summary>
        ''' <param name="type"></param>
        Private Shared Sub CreateTypeFor(type As Type)
            ' Error checking...
            ' Make sure that the type is an interface

            If Not type.IsInterface Then
                Throw New TypeIsNotAnInterfaceException(type)
            End If

            Dim typeBuilder As TypeBuilder = ModuleBuilder.DefineType("ImplOf" & type.Name, TypeAttributes.[Class] Or TypeAttributes.[Public])
            typeBuilder.AddInterfaceImplementation(type)

            ' Create Constructor
            Dim baseConstructorInfo As ConstructorInfo = GetType(Object).GetConstructor(New Type(-1) {})

            Dim constructorBuilder As ConstructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.[Public], CallingConventions.Standard, type.EmptyTypes)

            Dim ilGenerator As ILGenerator = constructorBuilder.GetILGenerator()
            ilGenerator.Emit(OpCodes.Ldarg_0)
            ' Load "this"
            ilGenerator.Emit(OpCodes.[Call], baseConstructorInfo)
            ' Call the base constructor
            ilGenerator.Emit(OpCodes.Ret)
            ' return
            ' Get a list of all methods, including methods in inherited interfaces
            ' The methods that aren't accessors and will need default implementations...  However,
            ' a property's accessors are also methods!
            Dim methods As New List(Of MethodInfo)()
            AddMethodsToList(methods, type)

            ' Get a list of all of the properties, including properties in inherited interfaces
            Dim properties As New List(Of PropertyInfo)()
            AddPropertiesToList(properties, type)

            ' Create accessors for each property
            For Each pi As PropertyInfo In properties
                Dim piName As String = pi.Name
                Dim propertyType As Type = pi.PropertyType

                ' Create underlying field; all properties have a field of the same type
                Dim field As FieldBuilder = typeBuilder.DefineField("_" & piName, propertyType, FieldAttributes.[Private])

                ' If there is a getter in the interface, create a getter in the new type
                Dim getMethod As MethodInfo = pi.GetGetMethod()
                If getMethod IsNot Nothing Then
                    ' This will prevent us from creating a default method for the property's 
                    ' getter
                    methods.Remove(getMethod)

                    ' Now we will generate the getter method
                    Dim methodBuilder As MethodBuilder = typeBuilder.DefineMethod(getMethod.Name, MethodAttributes.[Public] Or MethodAttributes.Virtual, propertyType, type.EmptyTypes)

                    ' The ILGenerator class is used to put op-codes (similar to assembly) into the
                    ' method
                    ilGenerator = methodBuilder.GetILGenerator()

                    ' These are the op-codes, (similar to assembly)
                    ilGenerator.Emit(OpCodes.Ldarg_0)
                    ' Load "this"
                    ilGenerator.Emit(OpCodes.Ldfld, field)
                    ' Load the property's underlying field onto the stack
                    ilGenerator.Emit(OpCodes.Ret)
                    ' Return the value on the stack
                    ' We need to associate our new type's method with the getter method in the 
                    ' interface
                    typeBuilder.DefineMethodOverride(methodBuilder, getMethod)
                End If

                ' If there is a setter in the interface, create a setter in the new type
                Dim setMethod As MethodInfo = pi.GetSetMethod()
                If setMethod IsNot Nothing Then
                    ' This will prevent us from creating a default method for the property's 
                    ' setter
                    methods.Remove(setMethod)

                    ' Now we will generate the setter method
                    Dim methodBuilder As MethodBuilder = typeBuilder.DefineMethod(setMethod.Name, MethodAttributes.[Public] Or MethodAttributes.Virtual, GetType(Void), New Type() {pi.PropertyType})

                    ' The ILGenerator class is used to put op-codes (similar to assembly) into the
                    ' method
                    ilGenerator = methodBuilder.GetILGenerator()

                    ' These are the op-codes, (similar to assembly)
                    ilGenerator.Emit(OpCodes.Ldarg_0)
                    ' Load "this"
                    ilGenerator.Emit(OpCodes.Ldarg_1)
                    ' Load "value" onto the stack
                    ilGenerator.Emit(OpCodes.Stfld, field)
                    ' Set the field equal to the "value" 
                    ' on the stack
                    ilGenerator.Emit(OpCodes.Ret)
                    ' Return nothing
                    ' We need to associate our new type's method with the setter method in the 
                    ' interface
                    typeBuilder.DefineMethodOverride(methodBuilder, setMethod)
                End If
            Next

            ' Create default methods.  These methods will essentially be no-ops; if there is a 
            ' return value, they will either return a default value or null
            For Each methodInfo As MethodInfo In methods
                ' Get the return type and argument types

                Dim returnType As Type = methodInfo.ReturnType

                Dim argumentTypes As New List(Of Type)()
                For Each parameterInfo As ParameterInfo In methodInfo.GetParameters()
                    argumentTypes.Add(parameterInfo.ParameterType)
                Next

                ' Define the method
                Dim methodBuilder As MethodBuilder = typeBuilder.DefineMethod(methodInfo.Name, MethodAttributes.[Public] Or MethodAttributes.Virtual, returnType, argumentTypes.ToArray())

                ' The ILGenerator class is used to put op-codes (similar to assembly) into the
                ' method
                ilGenerator = methodBuilder.GetILGenerator()

                ' If there's a return type, create a default value or null to return
                If returnType IsNot GetType(Void) Then
                    Dim localBuilder As LocalBuilder = ilGenerator.DeclareLocal(returnType)
                    ' this declares the local object, 
                    ' int, long, float, ect
                    ' load the value on the stack to 
                    ' return
                    ilGenerator.Emit(OpCodes.Ldloc, localBuilder)
                End If

                ilGenerator.Emit(OpCodes.Ret)
                ' return
                ' We need to associate our new type's method with the method in the interface
                typeBuilder.DefineMethodOverride(methodBuilder, methodInfo)
            Next

            ' Finally, after all the fields and methods are generated, create the type for use at
            ' run-time
            Dim createdType As Type = typeBuilder.CreateType()
            InterfaceImplementations(type) = createdType
        End Sub

        ''' <summary>
        ''' Helper method to get all MethodInfo objects from an interface.  This recurses to all 
        ''' sub-interfaces
        ''' </summary>
        ''' <param name="methods"></param>
        ''' <param name="type"></param>
        Private Shared Sub AddMethodsToList(methods As List(Of MethodInfo), type As Type)
            methods.AddRange(type.GetMethods())

            For Each subInterface As Type In type.GetInterfaces()
                AddMethodsToList(methods, subInterface)
            Next
        End Sub

        ''' <summary>
        ''' Helper method to get all PropertyInfo objects from an interface.  This recurses to all 
        ''' sub-interfaces
        ''' </summary>
        ''' <param name="properties"></param>
        ''' <param name="type"></param>
        Private Shared Sub AddPropertiesToList(properties As List(Of PropertyInfo), type As Type)
            properties.AddRange(type.GetProperties())

            For Each subInterface As Type In type.GetInterfaces()
                AddPropertiesToList(properties, subInterface)
            Next
        End Sub


    End Class
    ''' <summary>
    ''' Thrown when an attempt is made to create an object of a type that is not an interface
    ''' </summary>
    <Serializable> _
    Public Class TypeIsNotAnInterfaceException
        Inherits Exception
        Friend Sub New(type As Type)
            MyBase.New(String.Format(CultureInfo.CurrentCulture, My.Resources.Resources.TheInterfaceObjectFactoryOnlyWorksWithInte, type.FullName))
        End Sub
        Public Sub New()

        End Sub
        Public Sub New(ByVal message As String)
            MyBase.New(message)

        End Sub
        Public Sub New(ByVal message As String, ByVal innerException As Exception)
            MyBase.New(message, innerException)

        End Sub
        Protected Sub New(ByVal info As System.Runtime.Serialization.SerializationInfo, ByVal context As System.Runtime.Serialization.StreamingContext)
            MyBase.New(info, context)

        End Sub

    End Class
End Namespace
